| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- 'use client';
- import './style.scss';
- import { usePathname, useSearchParams } from 'next/navigation';
- import { useState, useEffect, useCallback, useRef } from 'react';
- import { BoardLayout, BoardSort, PostSearchType } from '@/constants/forum';
- import { fetchPostList } from '@/lib/api/forum/board';
- import Post from '@/types/forum/post';
- import Loading from '@/app/component/Loading';
- import Pagination from '@/app/component/Pagination';
- import PostListRequest from '@/dtos/request/forum/board/postListRequest';
- import BoardResponse from '@/dtos/response/forum/board/boardResponse';
- import PostListResponse from '@/dtos/response/forum/board/postListResponse';
- import PostWriteButtonfrom from '../_component/PostWriteButton';
- import HeaderContent from '../_component/HeaderContent';
- import FooterContent from '../_component/FooterContent';
- import DefaultListLayout from '../_component/DefaultListLayout';
- import AlbumListLayout from '../_component/AlbumListLayout';
- import QnAListLayout from '../_component/QnAListLayout';
- type ViewProps = {
- _query: {
- page: number;
- perPage: number;
- prefix?: number;
- sort?: BoardSort;
- search: PostSearchType;
- keyword?: string;
- },
- _board: BoardResponse,
- _postList: PostListResponse
- };
- export default function View({ _query, _board, _postList }: ViewProps)
- {
- const pathname = usePathname();
- const searchParams = useSearchParams();
- const [error, setError] = useState<string|null>(null);
- const [loading, setLoading] = useState<boolean>(false);
- const [total, setTotal] = useState<number>(_postList.total);
- const [speaker, setSpeaker] = useState<Post[]>(_postList.speaker);
- const [notice, setNotice] = useState<Post[]>(_postList.notice);
- const [list, setList] = useState<Post[]>(_postList.list);
- const [page, setPage] = useState<number>(_query.page);
- const [perPage, setPerPage] = useState<number>(_query.perPage);
- const [startIndex, setStartIndex] = useState<number>(0);
- const [boardPrefixID, setBoardPrefixID] = useState<number|undefined>(_query.prefix);
- const [sort, setSort] = useState<BoardSort|undefined>(_query.sort);
- const [search, setSearch] = useState<PostSearchType>(_query.search);
- const [keyword, setKeyword] = useState<string|undefined>(_query.keyword);
- const [params, setParams] = useState<Record<string, string>>({});
- const isMounted = useRef(false);
- useEffect(() => {
- if (error) {
- alert(error);
- setError(null);
- }
- }, [error]);
- // 상태 => URL 동기화
- useEffect(() => {
- // 기존 URL 파라미터
- const alreadyParams = new URLSearchParams(searchParams.toString());
- // URL 파라미터 덮어쓰기 및 삭제
- Object.entries(params).forEach(([k, v]) => {
- if (v) {
- alreadyParams.set(k, v);
- } else {
- alreadyParams.delete(k);
- }
- });
- const queryString = `?${alreadyParams.toString()}`;
- if (window.location.search !== queryString) {
- window.history.replaceState(null, '', `${pathname}${queryString}`);
- }
- }, [page, perPage, boardPrefixID, sort, search, keyword]);
- const handleFetchPosts = useCallback(async () => {
- try {
- setLoading(true);
- const res = await fetchPostList({
- boardID: _board.id,
- boardCode: _board.code,
- boardPrefixID: boardPrefixID,
- page,
- perPage,
- sort,
- search,
- keyword
- } as PostListRequest);
- if (!res.data) {
- setError('게시글을 불러올 수 없습니다.');
- } else {
- setTotal(res.data.total);
- setSpeaker(res.data.speaker);
- setNotice(res.data.notice);
- setList(res.data.list);
- }
- } catch (err: any) {
- setError(err.message || '알 수 없는 오류가 발생했습니다.');
- } finally {
- setLoading(false);
- }
- }, [_board.id, _board.code, boardPrefixID, page, perPage, sort, search, keyword]);
- const handleChange = (e: React.ChangeEvent<HTMLSelectElement|HTMLInputElement>) => {
- const { name, value } = e.target;
- let key = '';
- switch (name) {
- case 'boardPrefixID':
- setBoardPrefixID((Number(value) || undefined) as number);
- key = 'prefix';
- break;
- case 'sort':
- setSort(Number(value) as BoardSort);
- break;
- case 'perPage':
- setPerPage(Number(value));
- break;
- case 'search':
- setSearch(Number(value) as PostSearchType);
- break;
- case 'keyword':
- setKeyword(value);
- break;
- }
- if (['sort', 'perPage', 'search', 'keyword'].includes(name)) {
- key = name;
- }
- if (['boardPrefixID', 'perPage', 'search', 'keyword'].includes(name)) {
- handlePageChange(1);
- }
- setParams((prev) => ({ ...prev, [key]: value }));
- };
- const handleSearch = async (e: React.FormEvent) => {
- e.preventDefault();
- handleFetchPosts();
- };
- const handlePageChange = (page: number) => {
- setPage(page);
- setParams((prev) => ({ ...prev, page: String(page) }));
- };
- useEffect(() => {
- if (!isMounted.current) {
- isMounted.current = true;
- return;
- }
- handleFetchPosts();
- }, [page, perPage, boardPrefixID, sort]);
- useEffect(() => {
- setStartIndex(total - ((page - 1) * perPage));
- }, [total, list, page, perPage]);
- return (
- <div id='board'>
- {loading && <Loading />}
- <HeaderContent isEnabled={_board.boardMeta.list.showHeader} content={_board.boardMeta.list.headerContent } />
- <div className='list-header'>
- {/* 말머리 */}
- <section aria-label='말머리 선택'>
- <h1>{ _board.name }</h1>
- <article>
- <ul>
- <li>
- <label {...(!boardPrefixID ? { className: 'active' } : {})}>
- <input type='radio' name='boardPrefixID' value='' checked={boardPrefixID === null} onChange={handleChange} /> 전체
- </label>
- </li>
- {_board.boardPrefix.map((row, i) => (
- <li key={i}>
- <label {...(boardPrefixID === row.id ? { className: 'active' } : {})}
- style={row.color && row.color != '#000000' ? { background: row.color, color: '#f1f1f1' } : undefined}
- >
- <input type='radio' name='boardPrefixID' value={row.id} checked={boardPrefixID === row.id} onChange={handleChange}/>
- {row.name}
- </label>
- </li>
- ))}
- </ul>
- </article>
- </section>
- {/* 정렬 */}
- <section aria-label='게시글 정렬'>
- <select name='sort' value={sort ?? ''} title='게시글 정렬' onChange={handleChange}>
- <option value={BoardSort.CreatedAt}>최신순</option>
- <option value={BoardSort.Views}>조회순</option>
- <option value={BoardSort.Comments}>댓글순</option>
- <option value={BoardSort.Likes}>공감순</option>
- </select>
- </section>
- {/* 출력 수 */}
- <section aria-label='게시글 출력 수'>
- <select name='perPage' value={perPage} title='출력 수' onChange={handleChange}>
- <option value='10'>10개씩</option>
- <option value='20'>20개씩</option>
- <option value='30'>30개씩</option>
- <option value='50'>50개씩</option>
- <option value='100'>100개씩</option>
- </select>
- </section>
- </div>
- {/* 게시글 목록 */}
- {(() => {
- switch (_board.boardMeta.list.layout) {
- case BoardLayout.Media:
- return <AlbumListLayout boardListMeta={_board.boardMeta.list} speaker={speaker} notice={notice} list={list} startIndex={startIndex} onChange={setBoardPrefixID} />;
- case BoardLayout.QnA:
- return <QnAListLayout boardListMeta={_board.boardMeta.list} notice={notice} list={list} startIndex={startIndex} onChange={setBoardPrefixID} />;
- default:
- return <DefaultListLayout boardListMeta={_board.boardMeta.list} speaker={speaker} notice={notice} list={list} startIndex={startIndex} onChange={setBoardPrefixID} />;
- }
- })()}
- {/* 검색 */}
- <div className='list-footer'>
- <section aria-label='게시글 검색'>
- <form onSubmit={handleSearch} autoComplete='off'>
- <select name='search' value={search ?? ''} title='검색 구분' onChange={handleChange}>
- <option value={PostSearchType.Subject}>제목</option>
- <option value={PostSearchType.Content}>내용</option>
- <option value={PostSearchType.Author}>작성자</option>
- <option value={PostSearchType.Comment}>댓글</option>
- </select>
- <input type='text' name='keyword' value={keyword} placeholder='검색어를 입력해주세요.' onChange={handleChange} />
- <button type='submit' className='btn btn-default'>검색</button>
- </form>
- </section>
- {/* 글쓰기 버튼 */}
- <PostWriteButtonfrom alwaysShowButton={_board.boardMeta.list.alwaysShowWriteButton} boardCode={_board.code} />
- </div>
- <Pagination total={total} page={page} perPage={perPage} onChange={handlePageChange} />
- <FooterContent isEnabled={_board.boardMeta.list.showFooter} content={_board.boardMeta.list.footerContent } />
- </div>
- );
- }
|